package jia.ai;

import jia.game.GenericFlags;
import jia.core.Cartographer;
import org.apache.log4j.Logger;

import java.awt.*;
import java.util.*;
import java.util.List;


public final class AStar{
    
    private final Cartographer cartographer;
    private final List<NodeInfo> openNodeInfoSortedList;
    private final Map<Object, NodeInfo> nodeToNodeInfoMap;
    private NodeInfo bestNodeInfo;
    private double bestTotalCost;
    private NodeInfo goalNodeInfo;
    private boolean listEmpty;
    
    private static final Logger log = Logger.getLogger(AStar.class);
    
    /**
     * @param cartographer
     */
    public AStar(Cartographer cartographer){
        NullArgumentException.check(this.cartographer = cartographer);
        nodeToNodeInfoMap = new HashMap<Object, NodeInfo>();
        openNodeInfoSortedList = new LinkedList<NodeInfo>();
    }
    
    /**
     * @return
     */
    public boolean isGoalFound(){
        return goalNodeInfo != null;
    }
    
    /**
     * @return
     */
    public boolean isListEmpty(){
        return listEmpty;
    }
    
    /**
     * Creates an Iterator of points along a path to a specific goal
     *
     * @return
     */
    public Iterator<Object> getPath(){
        //create an empty list
        List<Object> pathList = new LinkedList<Object>();
        try {
            //assign the goal
            NodeInfo nodeInfo = goalNodeInfo;
            //if the goal cannot be determined, use the next best possible goal
            if(nodeInfo == null) {
                nodeInfo = bestNodeInfo;
            }
            
            //starting at the goal, get the parent nodes and assign one to the list
            while(nodeInfo != null) {
                NodeInfo parentNodeInfo = nodeInfo.getParentNodeInfo();
                if(parentNodeInfo != null) {
                    pathList.add(0, nodeInfo.getNode());
                }
                nodeInfo = parentNodeInfo;
            }
        } catch(Exception e) {
            if(GenericFlags.errorMessages) {
                log.error("An error occured while generating the path: " + e.getMessage());
            }
        }
        return pathList.iterator();
    }
    
    /**
     * @return
     */
    public Object getFirstStep(){
        NodeInfo nodeInfo = goalNodeInfo;
        Object node = null;
        try {
            if(nodeInfo == null) {
                nodeInfo = bestNodeInfo;
            }
            node = null;
            while(nodeInfo != null) {
                NodeInfo parentNodeInfo = nodeInfo.getParentNodeInfo();
                if(parentNodeInfo != null) {
                    node = nodeInfo.getNode();
                }
                nodeInfo = parentNodeInfo;
            }
        } catch(Exception e) {
            if(GenericFlags.errorMessages) {
                log.error("An error occured while getting the first step " + e.getMessage());
            }
        }
        return node;
    }
    
    /**
     * @param startNode
     */
    public void reset(Point startNode){
        goalNodeInfo = null;
        listEmpty = false;
        openNodeInfoSortedList.clear();
        nodeToNodeInfoMap.clear();
        NodeInfo nodeInfo = new NodeInfo(startNode);
        nodeToNodeInfoMap.put(startNode, nodeInfo);
        openNodeInfoSortedList.add(nodeInfo);
        bestTotalCost = Double.POSITIVE_INFINITY;
    }
    
    /**
     * @param startNode
     */
    public void setStartNode(Object startNode){
        listEmpty = false;
        openNodeInfoSortedList.clear();
        nodeToNodeInfoMap.clear();
        NodeInfo nodeInfo = new NodeInfo(startNode);
        nodeToNodeInfoMap.put(startNode, nodeInfo);
        openNodeInfoSortedList.add(nodeInfo);
        bestTotalCost = Double.POSITIVE_INFINITY;
    }
    
    /**
     * @return
     */
    public boolean loop(){
        try {
            if(openNodeInfoSortedList.isEmpty()) {
                if (GenericFlags.debugMessages){
                    log.debug("The openNodeInfoSortedList is empty");
                }
                listEmpty = true;
                return false;
            }
            NodeInfo nodeInfo = openNodeInfoSortedList.remove(0);
            Object node = nodeInfo.getNode();
            if (GenericFlags.debugMessages){
                log.debug("The node is " + node.toString());
            }
            if(cartographer.isGoalNode(node)) {
                if((goalNodeInfo == null) || (nodeInfo.getCostFromStart() < goalNodeInfo.getCostFromStart())) {
                    goalNodeInfo = nodeInfo;
                }
                return false;
            }
            if (GenericFlags.debugMessages){
                log.debug(node.toString());
            }
            Iterator iterator = cartographer.getAdjacentNodes(node);
            while(iterator.hasNext()) {
                if (GenericFlags.debugMessages){
                    log.debug("Searching....");
                }
                Object adjacentNode = iterator.next();
                double newCostFromStart = nodeInfo.getCostFromStart() + cartographer.getCostToAdjacentNode(node, adjacentNode);
                NodeInfo adjacentNodeInfo = nodeToNodeInfoMap.get(adjacentNode);
                if(adjacentNodeInfo == null) {
                    adjacentNodeInfo = new NodeInfo(adjacentNode);
                    nodeToNodeInfoMap.put(adjacentNode, adjacentNodeInfo);
                    openNodeInfoSortedList.add(adjacentNodeInfo);
                } else if(adjacentNodeInfo.getCostFromStart() <= newCostFromStart) {
                    continue;
                }
                adjacentNodeInfo.setParentNodeInfo(nodeInfo);
                adjacentNodeInfo.setCostFromStart(newCostFromStart);
                double totalCost = newCostFromStart + cartographer.estimateCostToGoal(adjacentNode);
                adjacentNodeInfo.setTotalCost(totalCost);
                if(totalCost < bestTotalCost) {
                    bestNodeInfo = adjacentNodeInfo;
                    bestTotalCost = totalCost;
                }
                Collections.sort(openNodeInfoSortedList);
            }
        } catch(Exception e) {
            if(GenericFlags.errorMessages) {
                log.error("An error was thrown in the looper " + e.getMessage());
            }
        }
        return true;
    }
}